package de.encryptor.types;


import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serializable;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;

import org.apache.commons.lang.ArrayUtils;
import org.hibernate.Hibernate;
import org.hibernate.HibernateException;
import org.hibernate.cfg.Environment;
import org.hibernate.type.NullableType;
import org.hibernate.usertype.ParameterizedType;
import org.hibernate.usertype.UserType;
import org.jasypt.encryption.pbe.PBEByteEncryptor;
import org.jasypt.encryption.pbe.StandardPBEByteEncryptor;
import org.jasypt.exceptions.EncryptionInitializationException;
import org.jasypt.hibernate.encryptor.HibernatePBEByteEncryptor;
import org.jasypt.hibernate.encryptor.HibernatePBEEncryptorRegistry;
import org.jasypt.encryption.pbe.PBEStringEncryptor;
import org.jasypt.encryption.pbe.StandardPBEStringEncryptor;
import org.codehaus.groovy.grails.commons.ConfigurationHolder as CH

public final class EncryptedBinary implements UserType, ParameterizedType {
	
	private static final int BLOCK_SIZE = 2048;
	
	private static NullableType nullableType = Hibernate.BINARY;
	static final int sqlType = nullableType.sqlType();
	static final int[] sqlTypes = [sqlType];
	private static def encryptorConfig = CH.config.encryptor
	
	private boolean initialized = false;
	private boolean useEncryptorName = false;
	
	private String encryptorName = null;
	private String algorithm = null;
	private String password = null;
	private Integer keyObtentionIterations = null;
	
	private static  PBEByteEncryptor encryptor = null;
	
	
	public int[] sqlTypes() {
		return ArrayUtils.clone(sqlTypes);
	}
	
	
	public Class returnedClass() {
		return byte[].class;
	}
	
	
	public boolean equals(Object x, Object y) 
	throws HibernateException {
		
		return (x == y) || 
		(x != null && y != null && java.util.Arrays.equals((byte[]) x, (byte[]) y));
		
	}
	
	
	public Object deepCopy(Object value)
	throws HibernateException {
		
		if (value == null) {
			return null;
		}
		byte[] valueBytes = (byte[]) value;
		byte[] copyBytes = new byte[valueBytes.length];
		System.arraycopy(valueBytes, 0, copyBytes, 0, valueBytes.length);
		return copyBytes;
		
	}
	
	
	public Object assemble(Serializable cached, Object owner)
	throws HibernateException {
		if (cached == null) {
			return null;
		}
		return deepCopy(cached);
	}
	
	
	public Serializable disassemble(Object value) 
	throws HibernateException {
		if (value == null) {
			return null;
		}
		return (Serializable) deepCopy(value);
	}
	
	
	public boolean isMutable() {
		return true;
	}
	
	
	public int hashCode(Object x)
	throws HibernateException {
		
		byte[] valueBytes = (byte[]) x;
		int result = 1;
		for (int i = 0; i < valueBytes.length; i++ ) {
			result = (result * 17) + valueBytes[i];
		}
		return result;
		
	}
	
	
	public Object replace(Object original, Object target, Object owner) 
	throws HibernateException {
		return (original == null)? null : deepCopy(original);
	}
	
	
	public Object nullSafeGet(ResultSet rs, String[] names, Object owner)
	throws HibernateException, SQLException {
		
		checkInitialization();
		
		byte[] encryptedValue = null;
		if (Environment.useStreamsForBinary()) {
			
			InputStream inputStream = rs.getBinaryStream(names[0]);
			if (rs.wasNull()) {
				return null;
			}
			
			ByteArrayOutputStream outputStream = 
					new ByteArrayOutputStream(BLOCK_SIZE);
			byte[] inputBuff = new byte[BLOCK_SIZE];
			try {
				int readBytes = 0;
				while (readBytes != -1) {
					readBytes = inputStream.read(inputBuff);
					if (readBytes != -1) {
						outputStream.write(inputBuff, 0, readBytes);
					}
				}
			} catch (IOException e) {
				throw new HibernateException(
				"IOException occurred reading a binary value", e);
			} finally {
				try {
					inputStream.close();
				} catch (IOException e) {
					// exception ignored
				}
				try {
					outputStream.close();
				} catch (IOException e) {
					// exception ignored
				}
			}
			
			encryptedValue = outputStream.toByteArray();
			
		} else {
			
			encryptedValue = rs.getBytes(names[0]);
			if (rs.wasNull()) {
				return null;
			}
			
		}	
		return this.encryptor.decrypt(encryptedValue);
		
	}
	
	
	public void nullSafeSet(PreparedStatement st, Object value, int index)	throws HibernateException, SQLException {
		
		checkInitialization();
		
		if (value == null) {
			st.setNull(index, sqlType);
		} else {
			byte[] encryptedValue = this.encryptor.encrypt((byte[]) value);
			if (Environment.useStreamsForBinary()) {
				st.setBinaryStream(
						index, 
						new ByteArrayInputStream(encryptedValue), 
						encryptedValue.length);
			} else {
				st.setBytes(index, encryptedValue);
			}
		}
		
	}
	
	
	public synchronized void setParameterValues(Properties parameters) {
		//do nothing
	}
	
	private synchronized void checkInitialization() {		
		if (!this.initialized) {
			this.encryptor = getEncryptor();
			this.initialized = true;
		}
		
	}
	
	public static StandardPBEByteEncryptor getEncryptor() {
		if (encryptor == null) {
			encryptor = new StandardPBEByteEncryptor();
			encryptor.algorithm=encryptorConfig.algorithm?encryptorConfig.algorithm:"PBEWithMD5AndTripleDES"
			encryptor.password=encryptorConfig.password?encryptorConfig.password:"jasypt"
			encryptor.keyObtentionIterations=encryptorConfig.keyObtentionIterations?encryptorConfig.keyObtentionIterations:1000
			encryptor.initialize();
		}
		return encryptor
	}
	
	
}
